# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError
from datetime import date


class BillOfMaterial(models.Model):
    """物料清单（BOM）- Bill of Material"""
    _name = 'bill.of.material'
    _description = '物料清单'
    _rec_name = 'display_name'
    _order = 'parent, sequence, component'
    
    # ==================== 基本信息 ====================
    company_id = fields.Many2one(
        'res.company',
        string='公司',
        default=lambda self: self.env.company,
        required=True,
        help='所属公司'
    )
    
    parent = fields.Char(
        string='父项物料',
        required=True,
        help='父项物料编号（可以是物料号或BOM编号）'
    )
    
    parent_item_id = fields.Many2one(
        'item.master',
        string='父项物料信息',
        help='关联的父项物料主数据（如果存在）'
    )
    
    component = fields.Char(
        string='组件物料',
        required=True,
        help='组件物料编号（必须是物料号或BOM编号）'
    )
    
    component_item_id = fields.Many2one(
        'item.master',
        string='组件物料信息',
        required=True,
        help='关联的组件物料主数据'
    )
    
    reference = fields.Char(
        string='参考编号',
        help='用于识别特定的父/子关系'
    )
    
    version = fields.Char(
        string='版本号',
        default='1.0',
        help='结构版本号'
    )
    
    sequence = fields.Integer(
        string='序号',
        default=10,
        help='显示顺序'
    )
    
    active = fields.Boolean(
        string='激活',
        default=True,
        help='是否激活此BOM'
    )
    
    # ==================== 数量与单位 ====================
    quantity_per = fields.Float(
        string='单位用量',
        digits='Product Unit of Measure',
        default=1.0,
        help='每个父项需要的组件数量'
    )
    
    product_uom_id = fields.Many2one(
        'uom.uom',
        string='单位',
        help='组件的计量单位'
    )
    
    um_conversion = fields.Float(
        string='单位换算',
        default=1.0,
        required=True,
        help='输入单位与库存单位之间的换算因子'
    )
    
    scrap_percent = fields.Float(
        string='报废率(%)',
        default=0.0,
        help='预期报废的百分比'
    )
    
    # 计算字段
    quantity_with_scrap = fields.Float(
        string='含报废用量',
        compute='_compute_quantity_with_scrap',
        store=True,
        help='包含报废率后的实际用量'
    )
    
    # ==================== 结构类型 ====================
    type = fields.Selection([
        ('B', '空白'),
        ('P', '计划'),
        ('O', '选配'),
        ('J', '联产'),
        ('F', '虚拟件'),
    ], string='结构类型', default='B', required=True, help='产品结构类型')
    
    is_mandatory = fields.Boolean(
        string='必需',
        default=True,
        help='组件是否为必需（非选配）'
    )
    
    phantom = fields.Boolean(
        string='虚拟件',
        default=False,
        help='是否为虚拟件（不保留库存，直接展开子件）'
    )
    
    # ==================== 有效期 ====================
    start_date = fields.Date(
        string='生效日期',
        default=fields.Date.today,
        required=True,
        help='此父/子关系生效的日期'
    )
    
    end_date = fields.Date(
        string='失效日期',
        help='此父/子关系失效的日期（为空表示永久有效）'
    )
    
    is_current = fields.Boolean(
        string='当前有效',
        compute='_compute_is_current',
        store=True,
        help='是否在当前日期有效'
    )
    
    # ==================== 工序信息 ====================
    ps_op = fields.Integer(
        string='工序',
        help='需要此组件的工序步骤代码'
    )
    
    operation_id = fields.Many2one(
        'production.operation',
        string='工序信息',
        help='关联的工序主数据'
    )
    
    position = fields.Char(
        string='位置',
        help='组件在产品中的位置标识'
    )
    
    # ==================== 子BOM ====================
    has_sub_bom = fields.Boolean(
        string='包含子BOM',
        compute='_compute_has_sub_bom',
        store=True,
        help='此组件是否有自己的BOM'
    )
    
    sub_bom_count = fields.Integer(
        string='子BOM数量',
        compute='_compute_sub_bom_count',
        help='此组件作为父项的BOM数量'
    )
    
    # ==================== 成本 ====================
    component_cost = fields.Float(
        string='组件成本',
        compute='_compute_component_cost',
        store=True,
        help='组件的单位成本'
    )
    
    line_cost = fields.Float(
        string='行成本',
        compute='_compute_line_cost',
        store=True,
        help='此BOM行的总成本（用量 × 成本）'
    )
    
    # ==================== 备注 ====================
    remarks = fields.Text(
        string='备注',
        help='关于此结构关系的用户备注'
    )
    
    notes = fields.Text(
        string='技术说明',
        help='技术说明和注意事项'
    )
    
    # ==================== 显示字段 ====================
    display_name = fields.Char(
        string='显示名称',
        compute='_compute_display_name',
        store=True
    )
    
    # ==================== 唯一性约束 ====================
    _sql_constraints = [
        ('bom_unique',
         'UNIQUE(company_id, parent, component, reference, start_date)',
         'BOM在同一公司、同一父项、同一组件、同一参考编号和同一生效日期下必须唯一！'),
        ('quantity_per_positive',
         'CHECK(quantity_per > 0)',
         '单位用量必须大于0！'),
        ('um_conversion_positive',
         'CHECK(um_conversion > 0)',
         '单位换算必须大于0！'),
    ]
    
    # ==================== 计算方法 ====================
    @api.depends('parent', 'component', 'version', 'start_date')
    def _compute_display_name(self):
        """计算显示名称"""
        for record in self:
            name = f"{record.parent} → {record.component}"
            if record.version:
                name += f" (v{record.version})"
            if record.reference:
                name += f" [{record.reference}]"
            record.display_name = name
    
    @api.depends('quantity_per', 'scrap_percent')
    def _compute_quantity_with_scrap(self):
        """计算含报废率的用量"""
        for record in self:
            if record.scrap_percent > 0:
                record.quantity_with_scrap = record.quantity_per * (1 + record.scrap_percent / 100)
            else:
                record.quantity_with_scrap = record.quantity_per
    
    @api.depends('start_date', 'end_date')
    def _compute_is_current(self):
        """计算是否当前有效"""
        today = date.today()
        for record in self:
            is_started = record.start_date <= today if record.start_date else False
            is_not_ended = not record.end_date or record.end_date >= today
            record.is_current = is_started and is_not_ended
    
    @api.depends('component')
    def _compute_has_sub_bom(self):
        """计算是否有子BOM"""
        for record in self:
            sub_bom = self.search([
                ('parent', '=', record.component),
                ('is_current', '=', True)
            ], limit=1)
            record.has_sub_bom = bool(sub_bom)
    
    @api.depends('component')
    def _compute_sub_bom_count(self):
        """计算子BOM数量"""
        for record in self:
            record.sub_bom_count = self.search_count([
                ('parent', '=', record.component),
                ('is_current', '=', True)
            ])
    
    @api.depends('component_item_id')
    def _compute_component_cost(self):
        """计算组件成本（需要物料主数据中有成本字段）"""
        for record in self:
            # 这里可以从物料主数据获取成本
            record.component_cost = 0.0
    
    @api.depends('quantity_with_scrap', 'component_cost')
    def _compute_line_cost(self):
        """计算行成本"""
        for record in self:
            record.line_cost = record.quantity_with_scrap * record.component_cost
    
    # ==================== 约束验证 ====================
    @api.constrains('parent', 'component', 'start_date', 'end_date')
    def _check_bom_validity(self):
        """验证BOM有效性"""
        for record in self:
            # 检查日期有效性
            if record.end_date and record.start_date > record.end_date:
                raise ValidationError('生效日期不能晚于失效日期！')
            
            # 检查自引用
            if record.parent == record.component:
                raise ValidationError(f'BOM不能自引用！父项和组件不能相同：{record.parent}')
            
            # 检查循环引用
            self._check_circular_reference(record)
    
    def _check_circular_reference(self, record, visited=None):
        """检查循环引用"""
        if visited is None:
            visited = set()
        
        if record.parent in visited:
            raise ValidationError(
                f'检测到循环引用！\n'
                f'物料链：{" → ".join(visited)} → {record.parent}\n'
                f'请检查BOM结构。'
            )
        
        visited.add(record.parent)
        
        # 查找当前组件作为父项的BOM
        sub_boms = self.search([
            ('parent', '=', record.component),
            ('company_id', '=', record.company_id.id),
            ('is_current', '=', True),
        ])
        
        for sub_bom in sub_boms:
            self._check_circular_reference(sub_bom, visited.copy())
    
    @api.onchange('parent')
    def _onchange_parent(self):
        """当父项改变时，尝试关联物料主数据"""
        if self.parent:
            item = self.env['item.master'].search([
                ('item_code', '=', self.parent),
                ('company_id', '=', self.company_id.id)
            ], limit=1)
            if item:
                self.parent_item_id = item.id
    
    @api.onchange('component')
    def _onchange_component(self):
        """当组件改变时，自动关联物料主数据"""
        if self.component:
            item = self.env['item.master'].search([
                ('item_code', '=', self.component),
                ('company_id', '=', self.company_id.id)
            ], limit=1)
            if item:
                self.component_item_id = item.id
                if item.um and not self.product_uom_id:
                    # 尝试找到对应的UOM
                    uom = self.env['uom.uom'].search([
                        ('name', '=', item.um)
                    ], limit=1)
                    if uom:
                        self.product_uom_id = uom.id
    
    # ==================== 业务方法 ====================
    def action_view_sub_bom(self):
        """查看子BOM"""
        self.ensure_one()
        return {
            'name': f'{self.component} 的子BOM',
            'type': 'ir.actions.act_window',
            'res_model': 'bill.of.material',
            'view_mode': 'list,form',
            'domain': [
                ('parent', '=', self.component),
                ('is_current', '=', True)
            ],
            'context': {'default_parent': self.component},
        }
    
    def get_bom_explosion(self, quantity=1.0, level=0, max_level=10):
        """
        BOM展开（多层）
        返回格式：[(level, parent, component, quantity, cost, ...)]
        """
        if level >= max_level:
            return []
        
        result = []
        for bom in self:
            # 当前BOM行
            result.append({
                'level': level,
                'parent': bom.parent,
                'component': bom.component,
                'component_name': bom.component_item_id.description1 if bom.component_item_id else '',
                'quantity_per': bom.quantity_per,
                'total_quantity': bom.quantity_with_scrap * quantity,
                'uom': bom.product_uom_id.name if bom.product_uom_id else '',
                'cost': bom.line_cost * quantity,
                'type': dict(bom._fields['type'].selection).get(bom.type),
                'is_mandatory': bom.is_mandatory,
                'phantom': bom.phantom,
                'reference': bom.reference,
                'remarks': bom.remarks,
            })
            
            # 如果不是虚拟件或需要继续展开
            if not bom.phantom or level < max_level - 1:
                # 查找子BOM
                sub_boms = self.search([
                    ('parent', '=', bom.component),
                    ('company_id', '=', bom.company_id.id),
                    ('is_current', '=', True),
                ])
                
                if sub_boms:
                    # 递归展开子BOM
                    sub_result = sub_boms.get_bom_explosion(
                        quantity=bom.quantity_with_scrap * quantity,
                        level=level + 1,
                        max_level=max_level
                    )
                    result.extend(sub_result)
        
        return result
    
    def action_explode_bom(self):
        """展开BOM（打开报表视图）"""
        self.ensure_one()
        
        # 创建展开向导记录（会自动触发展开）
        explosion = self.env['bill.of.material.explosion'].create({
            'parent_item': self.parent,
            'company_id': self.company_id.id,
            'quantity': 1.0,
            'max_level': 10,
        })
        
        return {
            'name': f'BOM展开 - {self.parent}',
            'type': 'ir.actions.act_window',
            'res_model': 'bill.of.material.explosion',
            'res_id': explosion.id,
            'view_mode': 'form',
            'target': 'new',
        }


class BillOfMaterialExplosion(models.TransientModel):
    """BOM展开向导（用于展示多层BOM）"""
    _name = 'bill.of.material.explosion'
    _description = 'BOM展开向导'
    
    parent_item = fields.Char(string='父项物料', required=True)
    company_id = fields.Many2one('res.company', string='公司', required=True)
    quantity = fields.Float(string='数量', default=1.0)
    max_level = fields.Integer(string='最大层级', default=10)
    
    line_ids = fields.One2many(
        'bill.of.material.explosion.line',
        'explosion_id',
        string='BOM明细'
    )
    
    @api.model_create_multi
    def create(self, vals_list):
        """创建时自动展开BOM"""
        records = super().create(vals_list)
        for record in records:
            record._compute_explosion()
        return records
    
    def _compute_explosion(self):
        """计算BOM展开"""
        self.ensure_one()
        if self.parent_item and self.company_id:
            boms = self.env['bill.of.material'].search([
                ('parent', '=', self.parent_item),
                ('company_id', '=', self.company_id.id),
                ('is_current', '=', True),
            ])
            
            if boms:
                explosion_data = boms.get_bom_explosion(
                    quantity=self.quantity,
                    max_level=self.max_level
                )
                
                # 创建明细行
                for seq, data in enumerate(explosion_data, start=1):
                    self.env['bill.of.material.explosion.line'].create({
                        'explosion_id': self.id,
                        'sequence': seq,
                        'level': data['level'],
                        'parent_item': data['parent'],
                        'component': data['component'],
                        'component_name': data['component_name'],
                        'quantity_per': data['quantity_per'],
                        'total_quantity': data['total_quantity'],
                        'uom': data['uom'],
                        'cost': data['cost'],
                        'type': data['type'],
                        'is_mandatory': data['is_mandatory'],
                        'reference': data['reference'],
                    })
    
    @api.onchange('parent_item', 'company_id', 'quantity', 'max_level')
    def _onchange_explode(self):
        """当参数改变时重新展开BOM"""
        if self.parent_item and self.company_id:
            boms = self.env['bill.of.material'].search([
                ('parent', '=', self.parent_item),
                ('company_id', '=', self.company_id.id),
                ('is_current', '=', True),
            ])
            
            if boms:
                explosion_data = boms.get_bom_explosion(
                    quantity=self.quantity,
                    max_level=self.max_level
                )
                
                # 清空现有行并创建新行
                self.line_ids = [(5, 0, 0)]
                lines = []
                for data in explosion_data:
                    lines.append((0, 0, {
                        'level': data['level'],
                        'parent_item': data['parent'],
                        'component': data['component'],
                        'component_name': data['component_name'],
                        'quantity_per': data['quantity_per'],
                        'total_quantity': data['total_quantity'],
                        'uom': data['uom'],
                        'cost': data['cost'],
                        'type': data['type'],
                        'is_mandatory': data['is_mandatory'],
                        'reference': data['reference'],
                    }))
                self.line_ids = lines


class BillOfMaterialExplosionLine(models.TransientModel):
    """BOM展开明细行"""
    _name = 'bill.of.material.explosion.line'
    _description = 'BOM展开明细行'
    _order = 'sequence'
    
    explosion_id = fields.Many2one('bill.of.material.explosion', string='展开')
    sequence = fields.Integer(string='序号', default=10)
    level = fields.Integer(string='层级')
    parent_item = fields.Char(string='父项')
    component = fields.Char(string='组件')
    component_name = fields.Char(string='组件名称')
    quantity_per = fields.Float(string='单位用量')
    total_quantity = fields.Float(string='总用量')
    uom = fields.Char(string='单位')
    cost = fields.Float(string='成本')
    type = fields.Char(string='类型')
    is_mandatory = fields.Boolean(string='必需')
    reference = fields.Char(string='参考')

